package com.guard.net;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.GZIPInputStream;
import javax.net.ssl.HttpsURLConnection;
import org.apache.http.Header;
import org.apache.http.HeaderElement;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpResponse;
import org.apache.http.HttpResponseInterceptor;
import org.apache.http.HttpVersion;
import org.apache.http.NameValuePair;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.ResponseHandler;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.conn.params.ConnManagerPNames;
import org.apache.http.conn.params.ConnPerRouteBean;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.HttpEntityWrapper;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.CoreProtocolPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.params.HttpProtocolParams;
import org.apache.http.protocol.HTTP;
import org.apache.http.protocol.HttpContext;
import android.annotation.SuppressLint;
import android.os.Build;
import android.util.Log;

/**
 * Apache HttpClient helper class for performing HTTP requests.
 * 
 * This class is intentionally *not* bound to any Android classes so that it is easier to develop and test. Use calls to this class inside
 * Android AsyncTask implementations (or manual Thread-Handlers) to make HTTP requests asynchronous and not block the UI Thread.
 * 
 * 
 * LICENSE: this code is based on code from the and-bookworm project ( http://code.google.com/p/and-bookworm/ ) and licensed under the free
 * Apache License, Version 2.0 See: http://www.apache.org/licenses/LICENSE-2.0 Many thanks to ccollings and the free software community.
 * 
 * 
 * 
 */
public class HttpManager {

    public static final String ENTITY_PARAM = "entity";
    private static final String CONTENT_ENCODING = "UTF-8";
    private static final String CONTENT_TYPE = "Content-Type";
    private static final int POST_TYPE = 1;
    private static final int GET_TYPE = 2;
    private static final String GZIP = "gzip";
    private static final String ACCEPT_ENCODING = "Accept-Encoding";

    public static final String MIME_FORM_ENCODED = "application/x-www-form-urlencoded";
    public static final String MIME_TEXT_PLAIN = "text/plain";
    public static final String HTTP_RESPONSE = "HTTP_RESPONSE";
    public static final String HTTP_RESPONSE_ERROR = "HTTP_RESPONSE_ERROR";

    // Establish client once, as static field with static setup block.
    // (This is a best practice in HttpClient docs - but will leave reference
    // until *process* stopped on Android.)
    private static DefaultHttpClient client;
    private static final String TAG = HttpManager.class.getName();
    private static final Integer CONNECTION_TIMEOUT = 20000;
    private static final Integer SO_TIMEOUT = 20000;

    static {
        setup();
    }

    @SuppressWarnings("deprecation")
    private static void disableConnectionReuseIfNecessary() {
        // HTTP connection reuse which was buggy pre-froyo
        if (Integer.parseInt(Build.VERSION.SDK) < Build.VERSION_CODES.FROYO) {
            System.setProperty("http.keepAlive", "false");
        }
    }

    public static Boolean LOCKED = false;

    public static synchronized void reset() {
        if (LOCKED) {
            return;
        }
        synchronized (client) {
            if (MODE.DEBUG) {
                Log.d(TAG, "reset()");
            }
            if (client != null) {
                client.getConnectionManager().closeExpiredConnections();
                client.clearRequestInterceptors();
                client.getConnectionManager().shutdown();
                client = null;
            }
            setup();
        }
    }

    @SuppressLint("DefaultLocale")
    private static void setup() {
        HttpParams params = new BasicHttpParams();
        params.setParameter(CoreProtocolPNames.PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
        params.setParameter(CoreProtocolPNames.HTTP_CONTENT_CHARSET, HTTP.UTF_8);
        params.setParameter(CoreProtocolPNames.USER_AGENT, "Apache-HttpClient/Android");
        params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, CONNECTION_TIMEOUT);
        params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT, SO_TIMEOUT);
        params.setParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, false);

        params.setIntParameter(ConnManagerPNames.MAX_TOTAL_CONNECTIONS, 30);
        params.setParameter(ConnManagerPNames.MAX_CONNECTIONS_PER_ROUTE, new ConnPerRouteBean(30));
        params.setParameter(HttpProtocolParams.USE_EXPECT_CONTINUE, false);

        HttpProtocolParams.setVersion(params, HttpVersion.HTTP_1_1);

        HttpsURLConnection.setFollowRedirects(false); // for HTTPS requests
        HttpURLConnection.setFollowRedirects(false); // for HTTP requests
        SchemeRegistry schemeRegistry = new SchemeRegistry();
        schemeRegistry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80));
        schemeRegistry.register(new Scheme("https", SSLSocketFactory.getSocketFactory(), 443));

        disableConnectionReuseIfNecessary();
        ThreadSafeClientConnManager cm = new ThreadSafeClientConnManager(params, schemeRegistry);
        client = new DefaultHttpClient(cm, params);
        // add gzip decompressor to handle gzipped content in responses
        // (default we *do* always send accept encoding gzip header in request)
        HttpManager.client.addResponseInterceptor(new HttpResponseInterceptor() {

            public void process(final HttpResponse response, final HttpContext context) throws HttpException, IOException {
                HttpEntity entity = response.getEntity();
                Header contentEncodingHeader = entity.getContentEncoding();
                if (contentEncodingHeader != null) {
                    HeaderElement[] codecs = contentEncodingHeader.getElements();
                    for (int i = 0; i < codecs.length; i++) {
                        if (codecs[i].getName().toLowerCase().contains(HttpManager.GZIP.toLowerCase())) {
                            response.setEntity(new GzipDecompressingEntity(response.getEntity()));
                            return;
                        }
                    }
                }
            }
        });
    }

    private final ResponseHandler<HttpResponseContainer> responseHandler;

    /**
     * Constructor.
     * 
     */
    public HttpManager() {
        responseHandler = new ResponseHandler<HttpResponseContainer>() {

            @Override
            public HttpResponseContainer handleResponse(HttpResponse response) {
                HttpResponseContainer info = new HttpResponseContainer();
                info.setHttpResponse(response);
                info.setStatusLine(response.getStatusLine());

                try {
                    InputStream stream = response.getEntity().getContent();
                    BufferedReader reader = new BufferedReader(new InputStreamReader(stream, CONTENT_ENCODING));
                    StringBuilder sb = new StringBuilder();
                    String line = null;
                    try {
                        while ((line = reader.readLine()) != null) {
                            sb.append(line + "\n");
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        try {
                            stream.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                    info.setBody(sb.toString());
                } catch (ClientProtocolException e) {
                    info.setBody(HttpManager.HTTP_RESPONSE_ERROR + " - " + e.getClass().getSimpleName() + " " + e.getMessage());
                } catch (IOException e) {
                    info.setBody(HttpManager.HTTP_RESPONSE_ERROR + " - " + e.getClass().getSimpleName() + " " + e.getMessage());
                }
                return info;
            }
        };
    }

    /**
     * Perform a simple HTTP GET operation.
     * 
     * @throws IOException
     * @throws ClientProtocolException
     * 
     */
    public HttpResponseContainer performGet(final String url) throws ClientProtocolException, IOException {
        return performRequest(null, url, null, null, null, null, HttpManager.GET_TYPE);
    }

    /**
     * Perform an HTTP GET operation with user/pass and headers.
     * 
     * @throws IOException
     * @throws ClientProtocolException
     * 
     */
    public HttpResponseContainer performGet(final String url, final String user, final String pass,
            final Map<String, String> additionalHeaders) throws ClientProtocolException, IOException {
        return performRequest(null, url, user, pass, additionalHeaders, null, HttpManager.GET_TYPE);
    }

    /**
     * Perform a simplified HTTP POST operation.
     * 
     * @throws IOException
     * @throws ClientProtocolException
     * 
     */
    public HttpResponseContainer performPost(final String url, final Map<String, String> params) throws ClientProtocolException,
            IOException {
        return performRequest(HttpManager.MIME_FORM_ENCODED, url, null, null, null, params, HttpManager.POST_TYPE);
    }

    /**
     * Perform an HTTP POST operation with user/pass, headers, request parameters, and a default content-type of
     * "application/x-www-form-urlencoded."
     * 
     * @throws IOException
     * @throws ClientProtocolException
     * 
     */
    public HttpResponseContainer performPost(final String url, final String user, final String pass,
            final Map<String, String> additionalHeaders, final Map<String, String> params) throws ClientProtocolException, IOException {
        return performRequest(HttpManager.MIME_FORM_ENCODED, url, user, pass, additionalHeaders, params, HttpManager.POST_TYPE);
    }

    /**
     * Perform an HTTP POST operation with flexible parameters (the complicated/flexible version of the method).
     * 
     * @throws IOException
     * @throws ClientProtocolException
     * 
     */
    public HttpResponseContainer performPost(final String contentType, final String url, final String user, final String pass,
            final Map<String, String> additionalHeaders, final Map<String, String> params) throws ClientProtocolException, IOException {
        return performRequest(contentType, url, user, pass, additionalHeaders, params, HttpManager.POST_TYPE);
    }

    //
    // private methods
    //
    private synchronized HttpResponseContainer performRequest(final String contentType, final String url, final String user,
            final String pass, final Map<String, String> headers, final Map<String, String> params, final int requestType)
            throws ClientProtocolException, IOException {

        // add user and pass to client credentials if present
        if ((user != null) && (pass != null)) {
            HttpManager.client.getCredentialsProvider().setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(user, pass));
        }

        // process headers using request interceptor
        final Map<String, String> sendHeaders = new HashMap<String, String>();
        // add encoding header for gzip if not present
        if (headers == null || !headers.containsKey(HttpManager.ACCEPT_ENCODING)) {
            sendHeaders.put(HttpManager.ACCEPT_ENCODING, HttpManager.GZIP);
        }
        if ((headers != null) && (headers.size() > 0)) {
            sendHeaders.putAll(headers);
        }
        if (requestType == HttpManager.POST_TYPE) {
            sendHeaders.put(HttpManager.CONTENT_TYPE, contentType);
        }

        // handle POST or GET request respectively
        HttpRequestBase method = null;
        if (requestType == HttpManager.POST_TYPE) {
            method = new HttpPost(url);
            // data - name/value params

            if (params.size() == 1 && params.containsKey(ENTITY_PARAM)) {
                HttpPost methodPost = (HttpPost) method;
                methodPost.setEntity(new StringEntity(params.get(ENTITY_PARAM), "UTF-8"));
            } else {
                List<NameValuePair> nvps = null;
                if ((params != null) && (params.size() > 0)) {
                    nvps = new ArrayList<NameValuePair>();
                    for (Map.Entry<String, String> entry : params.entrySet()) {
                        nvps.add(new BasicNameValuePair(entry.getKey(), entry.getValue()));
                    }
                }
                if (nvps != null) {
                    try {
                        HttpPost methodPost = (HttpPost) method;
                        methodPost.setEntity(new UrlEncodedFormEntity(nvps, HTTP.UTF_8));
                    } catch (UnsupportedEncodingException e) {
                        throw new RuntimeException("Error performing HTTP request: " + e.getMessage(), e);
                    }
                }
            }
        } else if (requestType == HttpManager.GET_TYPE) {
            method = new HttpGet(url);
        }

        for (String key : sendHeaders.keySet()) {
            if (!method.containsHeader(key)) {
                method.addHeader(key, sendHeaders.get(key));
            }
        }

        return execute(method);
    }

    private synchronized HttpResponseContainer execute(final HttpRequestBase method) throws ClientProtocolException, IOException {
        HttpResponseContainer response;

        try {

            if (MODE.DEBUG) {
                for (Header header : method.getAllHeaders()) {
                    Log.d(HttpManager.class.getName(), "Header: " + header);
                }
            }

            if (client == null) {
                if (MODE.DEBUG) {
                    Log.d(TAG, "http client was null in execute. Setting up new client");
                }
                HttpManager.reset();
            }

            // execute method returns?!? (rather than async) - do it here sync,
            // and
            // wrap async elsewhere
            response = HttpManager.client.execute(method, responseHandler);
            // HttpManager.client.removeRequestInterceptorByClass(HttpRequestInterceptor.class);
        } finally {
            if (client != null) {
                ThreadSafeClientConnManager tscm = (ThreadSafeClientConnManager) client.getConnectionManager();
                if (MODE.DEBUG) {
                    Log.d(TAG, "connections in pool=" + tscm.getConnectionsInPool());
                }

                client.getConnectionManager().closeExpiredConnections();
            }
        }
        return response;
    }

    static class GzipDecompressingEntity extends HttpEntityWrapper {

        public GzipDecompressingEntity(final HttpEntity entity) {
            super(entity);
        }

        @Override
        public InputStream getContent() throws IOException, IllegalStateException {
            // the wrapped entity's getContent() decides about repeatability
            InputStream wrappedin = wrappedEntity.getContent();
            return new GZIPInputStream(wrappedin);
        }

        @Override
        public long getContentLength() {
            // length of ungzipped content is not known
            return -1;
        }
    }
}
